// Minimal HTTP server to preview the frontend and expose read-only APIs
import http from 'node:http';
import fs from 'node:fs';
import path from 'node:path';
import url from 'node:url';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
dotenv.config({ path: path.resolve(__dirname, '.env') });

import nodemailer from 'nodemailer';

const ROOT = 'd:/Trae/reportsdata';
const ACTIVE = path.join(ROOT, 'active');
const LOGS = path.join(ROOT, 'logs');
const PUBLIC = path.join(process.cwd(), 'public');

function ensure(p) { if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true }); }
ensure(ACTIVE); ensure(LOGS); ensure(PUBLIC);

function sendJson(res, obj) {
  res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
  res.end(JSON.stringify(obj));
}

function send404(res) { res.writeHead(404); res.end('Not found'); }

function readBundle(ts_code) {
  const f = path.join(ACTIVE, `${ts_code}.json`);
  if (!fs.existsSync(f)) return null;
  return JSON.parse(fs.readFileSync(f, 'utf-8'));
}

// --- Logging helpers ---
function _nowTs() { return new Date().toISOString().slice(11, 19); }
function _todayYmd() { const d = new Date(); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${y}${m}${day}`; }
function writeLog(scope, message) {
  const f = path.join(LOGS, `${scope}_${_todayYmd()}.log`);
  fs.appendFileSync(f, `[${_nowTs()}] ${message}\n`);
}

// --- Ingest helpers (JS runtime, minimal duplication) ---
const TUSHARE_API = 'https://api.tushare.pro';
function getToken() {
  const t = process.env.TUSHARE_TOKEN;
  if (!t) throw new Error('缺少 TUSHARE_TOKEN 环境变量，请设置后重启');
  return t;
}
async function tsPost(api_name, params, fields) {
  const body = { api_name, token: getToken(), params };
  if (fields && fields.length) body.fields = fields.join(',');
  const resp = await fetch(TUSHARE_API, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
  const json = await resp.json();
  if (json.code !== 0) throw new Error(json.msg || 'Tushare error');
  return json.data || { fields: [], items: [] };
}
async function fetchTickers() {
  const { fields, items } = await tsPost('stock_basic', { list_status: 'L' }, ['ts_code', 'name']);
  const iC = fields.indexOf('ts_code'); const iN = fields.indexOf('name');
  return items.map(r => ({ ts_code: String(r[iC]), name: iN >= 0 ? String(r[iN]) : undefined }));
}
async function fetchDaily(ts_code, start_date, end_date) {
  const { fields, items } = await tsPost('daily', { ts_code, start_date, end_date });
  return items.map(row => {
    const obj = {}; fields.forEach((f, i) => obj[f] = row[i]);
    return { trade_date: String(obj.trade_date), open: Number(obj.open), high: Number(obj.high), low: Number(obj.low), close: Number(obj.close), vol: obj.vol !== undefined ? Number(obj.vol) : undefined };
  });
}
async function fetchAdjFactor(ts_code, start_date, end_date) {
  const { fields, items } = await tsPost('adj_factor', { ts_code, start_date, end_date }, ['trade_date', 'adj_factor']);
  const iD = fields.indexOf('trade_date'); const iF = fields.indexOf('adj_factor');
  const m = new Map(); items.forEach(r => { const d = String(r[iD]); const f = Number(r[iF]); if (!isNaN(f)) m.set(d, f); });
  return m;
}
function sortByDate(series) { return [...series].sort((a, b) => a.trade_date.localeCompare(b.trade_date)); }
function applyQfq(data, factorMap) {
  if (!data.length) return [];
  const latestDate = data.map(d => d.trade_date).sort((a, b) => b.localeCompare(a))[0];
  const latestFactor = factorMap.get(latestDate);
  if (!latestFactor || isNaN(latestFactor)) return data.map(d => ({ ...d }));
  return data.map(row => {
    const f = factorMap.get(row.trade_date);
    if (f && !isNaN(f)) {
      const ratio = f / latestFactor;
      const adj = v => (v == null || isNaN(Number(v))) ? v : Number(v) * ratio;
      return { trade_date: row.trade_date, open: adj(row.open), high: adj(row.high), low: adj(row.low), close: adj(row.close), vol: row.vol };
    }
    return { ...row };
  });
}
function daysBetween(a, b) {
  const ad = new Date(Date.UTC(Number(a.slice(0, 4)), Number(a.slice(4, 6)) - 1, Number(a.slice(6, 8))));
  const bd = new Date(Date.UTC(Number(b.slice(0, 4)), Number(b.slice(4, 6)) - 1, Number(b.slice(6, 8))));
  return Math.round((bd.getTime() - ad.getTime()) / (24 * 3600 * 1000));
}
function ymdAddDays(ymd, k) {
  const d = new Date(Date.UTC(Number(ymd.slice(0, 4)), Number(ymd.slice(4, 6)) - 1, Number(ymd.slice(6, 8))));
  d.setUTCDate(d.getUTCDate() + k);
  const y = d.getUTCFullYear(); const m = String(d.getUTCMonth() + 1).padStart(2, '0'); const day = String(d.getUTCDate()).padStart(2, '0');
  return `${y}${m}${day}`;
}
function interpolateGaps(series, maxGapDays = 5) {
  if (series.length <= 1) return series;
  const out = [];
  for (let i = 0; i < series.length - 1; i++) {
    const a = series[i]; const b = series[i + 1]; out.push(a);
    const gap = daysBetween(a.trade_date, b.trade_date) - 1; if (gap <= 0 || gap > maxGapDays) continue;
    const steps = gap; const logR = Math.log(b.close / a.close); const stepR = logR / (steps + 1);
    const openStep = (b.open - a.open) / (steps + 1); const highStep = (b.high - a.high) / (steps + 1); const lowStep = (b.low - a.low) / (steps + 1);
    for (let k = 1; k <= steps; k++) {
      const trade_date = ymdAddDays(a.trade_date, k); const close = a.close * Math.exp(stepR * k);
      let open = a.open + openStep * k; let high = a.high + highStep * k; let low = a.low + lowStep * k;
      const maxOC = Math.max(open, close); const minOC = Math.min(open, close);
      if (high < maxOC) high = maxOC; if (low > minOC) low = minOC;
      out.push({ trade_date, open, high, low, close, vol: undefined });
    }
  }
  out.push(series[series.length - 1]);
  return out;
}
function computeATR(ohlc, period) {
  const tr = []; for (let i = 0; i < ohlc.length; i++) { const h = ohlc[i].high, l = ohlc[i].low, cPrev = i > 0 ? ohlc[i - 1].close : ohlc[i].close; tr.push(Math.max(h - l, Math.abs(h - cPrev), Math.abs(l - cPrev))); }
  const atr = Array(ohlc.length).fill(NaN); if (ohlc.length < period) return atr; let sum = 0; for (let i = 0; i < period; i++) sum += tr[i];
  atr[period - 1] = sum / period; for (let i = period; i < tr.length; i++) atr[i] = (atr[i - 1] * (period - 1) + tr[i]) / period; return atr;
}
function computeRollingStd(closes, period) {
  const out = Array(closes.length).fill(NaN); if (closes.length < period) return out; let sum = 0, sumSq = 0;
  for (let i = 0; i < period; i++) { sum += closes[i]; sumSq += closes[i] * closes[i]; }
  out[period - 1] = Math.sqrt(Math.max((sumSq / period) - Math.pow(sum / period, 2), 0));
  for (let i = period; i < closes.length; i++) { sum += closes[i] - closes[i - period]; sumSq += closes[i] * closes[i] - closes[i - period] * closes[i - period]; const v = (sumSq / period) - Math.pow(sum / period, 2); out[i] = Math.sqrt(Math.max(v, 0)); }
  return out;
}
function rollingMax(values, period) {
  const out = Array(values.length).fill(NaN); const deque = []; for (let i = 0; i < values.length; i++) { while (deque.length && deque[0] <= i - period) deque.shift(); while (deque.length && values[deque[deque.length - 1]] <= values[i]) deque.pop(); deque.push(i); if (i >= period - 1) out[i] = values[deque[0]]; } return out;
}
function rollingMin(values, period) {
  const out = Array(values.length).fill(NaN); const deque = []; for (let i = 0; i < values.length; i++) { while (deque.length && deque[0] <= i - period) deque.shift(); while (deque.length && values[deque[deque.length - 1]] >= values[i]) deque.pop(); deque.push(i); if (i >= period - 1) out[i] = values[deque[0]]; } return out;
}
function attachMetrics(series, atrPeriod, stdPeriod, holdingDays) {
  if (!series.length) return series; const ohlc = series.map(d => ({ high: d.high, low: d.low, close: d.close })); const closes = series.map(d => d.close); const highs = series.map(d => d.high); const lows = series.map(d => d.low);
  const atr = computeATR(ohlc, atrPeriod); const std = computeRollingStd(closes, stdPeriod); const rh = rollingMax(highs, holdingDays); const rl = rollingMin(lows, holdingDays);
  return series.map((d, i) => ({ ...d, atr: isNaN(atr[i]) ? null : Number(atr[i].toFixed(6)), std: isNaN(std[i]) ? null : Number(std[i].toFixed(6)), rolling_high: isNaN(rh[i]) ? null : Number(rh[i].toFixed(6)), rolling_low: isNaN(rl[i]) ? null : Number(rl[i].toFixed(6)) }));
}
function writeBundle(ts_code, bundle) {
  const f = path.join(ACTIVE, `${ts_code}.json`);
  fs.writeFileSync(f, JSON.stringify(bundle));
}
function mergeSeries(a, b) {
  const map = new Map(); a.forEach(r => map.set(r.trade_date, r)); b.forEach(r => map.set(r.trade_date, r)); const merged = Array.from(map.values()).sort((x, y) => x.trade_date.localeCompare(y.trade_date)); return merged;
}
function todayYmd() { const d = new Date(); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${y}${m}${day}`; }
function fiveYearsAgoYmd() { const d = new Date(); d.setFullYear(d.getFullYear() - 5); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${y}${m}${day}`; }
async function ingestInitAll() {
  const start = fiveYearsAgoYmd(); const end = todayYmd();
  const tickers = await fetchTickers();
  for (const meta of tickers) {
    try {
      const raw = await fetchDaily(meta.ts_code, start, end);
      const adj = await fetchAdjFactor(meta.ts_code, start, end);
      let series = applyQfq(raw, adj); series = sortByDate(series); series = interpolateGaps(series, 5); series = attachMetrics(series, 14, 20, 252);
      const bundle = { meta, series, summary: { latest_date: series.at(-1)?.trade_date || end, highest_price_prefix: Math.max(...series.map(s => s.high)), lowest_price_prefix: Math.min(...series.map(s => s.low)), currency: 'CNY', adjusted: 'qfq', source: 'tushare' } };
      writeBundle(meta.ts_code, bundle);
      writeLog('ingest', `init ${meta.ts_code} done (${series.length} rows)`);
    } catch (e) { console.error('init error', meta.ts_code, e?.message || e); }
  }
}
async function ingestUpdateAll() {
  const end = todayYmd();
  const files = fs.readdirSync(ACTIVE).filter(f => f.endsWith('.json'));
  if (files.length === 0) return; // nothing to update
  const tickers = files.map(f => ({ ts_code: path.basename(f, '.json') }));
  let ok = 0, fail = 0;
  for (const meta of tickers) {
    try {
      const existing = readBundle(meta.ts_code);
      const start = existing?.summary?.latest_date || fiveYearsAgoYmd();
      const raw = await fetchDaily(meta.ts_code, start, end);
      const adj = await fetchAdjFactor(meta.ts_code, start, end);
      let series = applyQfq(raw, adj); series = sortByDate(series); series = interpolateGaps(series, 5); series = attachMetrics(series, 14, 20, 252);
      const merged = existing ? mergeSeries(existing.series || [], series) : series;
      const bundle = { meta, series: merged, summary: { latest_date: merged.at(-1)?.trade_date || end, highest_price_prefix: Math.max(...merged.map(s => s.high)), lowest_price_prefix: Math.min(...merged.map(s => s.low)), currency: 'CNY', adjusted: 'qfq', source: 'tushare' } };
      writeBundle(meta.ts_code, bundle);
      ok++;
      writeLog('ingest', `update ${meta.ts_code} ok (+${series.length} rows)`);
    } catch (e) { console.error('update error', meta.ts_code, e?.message || e); }
  }
  return { ok, fail };
}
function scheduleDailyUpdate() {
  const now = new Date(); const next = new Date(now); next.setHours(0, 10, 0, 0); if (next <= now) next.setDate(next.getDate() + 1);
  const delay = next.getTime() - now.getTime();
  setTimeout(async function run() {
    try { const { ok = 0, fail = 0 } = await ingestUpdateAll() || {}; writeLog('ingest', `scheduled daily update: ok=${ok}, fail=${fail}`); await notifyEmail(`A股每日更新结果`, `成功: ${ok}  失败: ${fail}  时间: ${new Date().toLocaleString()}`); } catch (e) { console.error('scheduled update error', e?.message || e); writeLog('ingest', `scheduled update error: ${e?.message || e}`); }
    scheduleDailyUpdate();
  }, delay);
}

// --- Email notification ---
let transporter = null;
function initMailer() {
  const host = process.env.SMTP_HOST; const port = process.env.SMTP_PORT ? Number(process.env.SMTP_PORT) : 465;
  const user = process.env.SMTP_USER; const pass = process.env.SMTP_PASS; const secure = String(process.env.SMTP_SECURE || 'true') === 'true';
  if (!host || !user || !pass) { console.warn('Mailer not configured: set SMTP_HOST/SMTP_PORT/SMTP_USER/SMTP_PASS/SMTP_SECURE'); return; }
  transporter = nodemailer.createTransport({ host, port, secure, auth: { user, pass } });
}
async function notifyEmail(subject, text) {
  try {
    if (!transporter) initMailer();
    if (!transporter) return;
    const from = process.env.MAIL_FROM || process.env.SMTP_USER;
    const to = process.env.MAIL_TO;
    if (!to) { console.warn('MAIL_TO not set, skip email'); return; }
    await transporter.sendMail({ from, to, subject, text });
  } catch (e) { console.warn('notifyEmail error:', e?.message || e); }
}

const server = http.createServer((req, res) => {
  const parsed = url.parse(req.url, true);
  const { pathname, query } = parsed;
  if (pathname === '/') {
    const f = path.join(PUBLIC, 'index.html');
    if (!fs.existsSync(f)) return send404(res);
    res.writeHead(200, { 'Content-Type': 'text/html' });
    return fs.createReadStream(f).pipe(res);
  }
  if (pathname === '/api/symbols') {
    const files = fs.readdirSync(ACTIVE).filter(f => f.endsWith('.json'));
    const symbols = files.map(f => ({ ts_code: path.basename(f, '.json') }));
    return sendJson(res, symbols);
  }
  if (pathname === '/api/data') {
    const ts_code = query.ts_code;
    if (!ts_code) return sendJson(res, { error: 'Missing ts_code' });
    const bundle = readBundle(ts_code);
    if (!bundle) return sendJson(res, { error: 'No data' });
    let series = bundle.series || [];
    const start = query.start; const end = query.end;
    if (start) series = series.filter(r => r.trade_date >= start);
    if (end) series = series.filter(r => r.trade_date <= end);
    const holdingDays = query.holdingDays ? Number(query.holdingDays) : 252;
    const processedSeries = attachMetrics(series, holdingDays);
    return sendJson(res, { meta: bundle.meta, summary: bundle.summary, series: processedSeries });
  }
  if (pathname === '/api/logs') {
    const date = query.date || new Date().toISOString().slice(0, 10).replace(/-/g, '');
    const f = path.join(LOGS, `ingest_${date}.log`);
    if (!fs.existsSync(f)) return sendJson(res, { date, lines: [] });
    const body = fs.readFileSync(f, 'utf-8');
    return sendJson(res, { date, lines: body.split('\n').filter(Boolean) });
  }
  // ingest triggers (manual)
  if (pathname === '/api/ingest/init') {
    ingestInitAll().then(() => { writeLog('ingest', 'manual init completed'); sendJson(res, { ok: true }); }).catch(err => { writeLog('ingest', `manual init error: ${err?.message || err}`); sendJson(res, { error: err?.message || String(err) }); });
    return;
  }
  if (pathname === '/api/ingest/update') {
    ingestUpdateAll().then(async (r) => { writeLog('ingest', `manual update: ok=${r?.ok || 0}, fail=${r?.fail || 0}`); await notifyEmail('手动增量更新结果', `成功: ${r?.ok || 0} 失败: ${r?.fail || 0}`); sendJson(res, { ok: true, result: r }); }).catch(err => { writeLog('ingest', `manual update error: ${err?.message || err}`); sendJson(res, { error: err?.message || String(err) }); });
    return;
  }
  // static assets
  if (pathname && pathname.startsWith('/assets/')) {
    const f = path.join(PUBLIC, pathname.replace('/assets/', ''));
    if (!fs.existsSync(f)) return send404(res);
    res.writeHead(200);
    return fs.createReadStream(f).pipe(res);
  }
  send404(res);
});

const PORT = process.env.PORT ? Number(process.env.PORT) : 5173;
server.listen(PORT, () => {
  console.log(`Server running at http://localhost:${PORT}/`);
  try { getToken(); scheduleDailyUpdate(); console.log('Daily auto-update scheduled at ~00:10'); } catch (e) { console.warn('Auto-update disabled:', e?.message || e); }
  initMailer();
});